/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package axil.framework.template;

import axil.etc.NameValue;
import axil.framework.error.ApplicationException;

import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

import static axil.framework.Functions.*;


/**
 * A compiler for the template syntax for Axil templates.
 */
class TemplateCompiler {
	private static final char EOF = '\u0000';
	// TODO: Replace this with AccessPath support instead
	private static final TemplateAdapter script = null;
	private static final TemplateAdapter compound = null;

	private String reference;
	private LineNumberReader reader;
	private StringBuilder buffer;
	private List<Instruction> code;


	TemplateCompiler(String reference, Reader reader) {
		this.reference = reference;
		this.reader = new LineNumberReader(reader);
		this.buffer = new StringBuilder();
		this.code = new ArrayList<Instruction>();
	}


	/**
	 * Compile the template given in the reader and return a new template. The
	 * object returned is never null. An application exception is thrown if the
	 * template contains an error.
	 */
	public Template compile() {
		while(true) {
			char ch = next();
			switch(ch) {
			case '[':
				char c2 = next();
				if (c2 == '-') {
					char c3 = next();
					if (c3 == '-') {
						emit();
						embed();
					} else {
						put('[');
						put(c2);
						put(c3);
					}
				} else {
					put('[');
					put(c2);
				}
				break;

			case '\\':
				escape();
				break;

			case EOF:
				emit();
				return new Template(
				           code.toArray(new Instruction[code.size()]));

			default:
				put(ch);
			}
		}
	}


	private void escape() {
		char ch = next();
		switch(ch) {
		case '\\':
		case '[':
			put(ch);
			break;

		default:
			put('\\');
			put(ch);
		}
	}


	private void embed() {
		while(true) {
			char ch = next();
			switch(ch) {
			case '-':
				char c2 = next();
				if (c2 == '-') {
					char c3 = next();
					if (c3 == ']') {
						decode(content().trim());
						return;
					} else {
						put('-');
						put(c2);
						put(c3);
					}
				} else {
					put('-');
					put(c2);
				}
				break;

			case EOF:
				throw problem("template-embedded-eof");

			default:
				put(ch);
			}
		}
	}


	private void decode(String encoded) {
		if (special(encoded)) {
			if (script == null) {
				throw problem("unrecognized-syntax", nv("syntax", encoded));
			}
			code.add(script.compile(encoded));
		} else {
			if (encoded.indexOf('.') >= 0) {
				if (compound == null) {
					throw problem("unrecognized-syntax", nv("syntax", encoded));
				}
				code.add(compound.compile(encoded));
			} else {
				code.add(new SimpleVariable(encoded));
			}
		}
	}


	/**
	 * Tell if the given text contains any special characters. These characters
	 * are an indication that an additional external grammar is being used. We
	 * do not attempt to interpret it, just forward it.
	 */
	private boolean special(String text) {
		final int size = text.length();
		for (int i = 0; i < size; i++) {
			char ch = text.charAt(i);
			if ("`~!@#$%^&*()_=+<>,/?{}| ".indexOf(ch) >= 0) {
				return true;
			}
		}
		return false;
	}


	private ApplicationException problem(String key) {
		return error(axil(), key, nv("line", reader.getLineNumber()),
		             nv("source", reference));
	}


	private ApplicationException problem(String key, NameValue detail) {
		return error(axil(), key, nv("line", reader.getLineNumber()),
		             nv("source", reference), detail);
	}


	private char next() {
		try {
			int ch = reader.read();
			return ch < 0 ? EOF : (char)ch;
		} catch (IOException e) {
			throw abort(e);
		}
	}


	private void put(char ch) {
		buffer.append(ch);
	}


	private String content() {
		String s = buffer.toString();
		buffer.setLength(0);
		return s;
	}


	private void emit() {
		if (buffer.length() > 0) {
			code.add(new StaticText(content()));
		}
	}
}
